#
# Copyright 2020, Data61, CSIRO (ABN 41 687 119 230)
#
# SPDX-License-Identifier: BSD-2-Clause
#

cmake_minimum_required(VERSION 3.7.2)

project(elfloader C ASM)

if(KernelArchX86)
    # This project is only used on Arm or RISC-V
    return()
endif()

include(${KERNEL_FLAGS_PATH})
include(cpio)

set(configure_string "")

config_choice(
    ElfloaderImage
    ELFLOADER_IMAGE
    "Boot image type"
    "elf;ElfloaderImageELF;IMAGE_ELF;KernelArchARM OR KernelArchRiscV"
    "binary;ElfloaderImageBinary;IMAGE_BINARY;KernelArchARM OR KernelArchRiscV"
    "efi;ElfloaderImageEFI;IMAGE_EFI;KernelArchARM"
    "uimage;ElfloaderImageUimage;IMAGE_UIMAGE;KernelArchARM"
)

config_choice(
    ElfloaderMode
    ELFLOADER_MODE
    "seL4 mode"
    "secure supervisor;ElfloaderModeSSup;ARM_S_SUPERVISOR_MODE;KernelPlatImx6"
    "monitor;ElfloaderModeMonitor;ARM_MONITOR_MODE;KernelPlatformTK1 OR KernelPlatImx6"
    "hypervisor;ElfloaderModeHyp;ARM_HYPERVISOR_MODE;KernelPlatformTK1"
    "non-secure supervisor;ElfloaderModeNSSup;ARM_NS_SUPERVISOR_MODE;KernelPlatformTK1 OR KernelPlatImx6"
)

config_option(
    ElfloaderMonitorHook ARM_MONITOR_HOOK "Install SMC call handlers in monitor mode."
    DEFAULT OFF
    DEPENDS "KernelPlatformTK1 OR KernelPlatImx6"
)

config_option(
    ElfloaderGPTPtimersNSPL1Access GPT_PTIMER_NS_PL1_ACCESS
    "Enable the GPT physical timer access for NS PL1"
    DEFAULT ON
    DEPENDS "ElfloaderModeNSSup"
    DEFAULT_DISABLED OFF
)

config_option(
    ElfloaderErrata764369 ARM_ERRATA_764369
    "Work around for a Cortex-A9 errata. Derived from Linux kernel."
    DEFAULT ON
    DEPENDS "KernelArmCortexA9"
    DEFAULT_DISABLED OFF
)

config_choice(
    ElfloaderHashInstructions
    HASH_INSTRUCTIONS
    "Perform a SHA256/MD5 Hash of the of each elf file that the elfloader checks on load"
    "hash_none;ElfloaderHashNone;HASH_NONE"
    "hash_sha;ElfloaderHashSHA;HASH_SHA"
    "hash_md5;ElfloaderHashMD5;HASH_MD5"
)

config_option(
    ElfloaderIncludeDtb ELFLOADER_INCLUDE_DTB
    "Include DTB in the CPIO in case bootloader doesn't provide one"
    DEFAULT ON
    DEPENDS "KernelArchARM OR KernelArchRiscV"
    DEFAULT_DISABLED OFF
)

config_option(
    ElfloaderRootserversLast ELFLOADER_ROOTSERVERS_LAST
    "Place the rootserver images at the end of memory"
    DEFAULT OFF
    DEFAULT_DISABLED OFF
)

config_option(
    ElfloaderArmV8LeaveAarch64 ELFLOADER_ARMV8_LEAVE_AARCH64
    "Insert aarch64 code to switch to aarch32. Requires the elfloader to be in EL2"
    DEFAULT OFF
    DEPENDS KernelArchArmV8a
)

add_config_library(elfloader "${configure_string}")

add_compile_options(-D_XOPEN_SOURCE=700 -ffreestanding -Wall -Werror -W -Wextra)
set(linkerScript "${CMAKE_CURRENT_LIST_DIR}/src/arch-${KernelArch}/linker.lds")
if(KernelArchRiscV)
    add_compile_options(-mcmodel=medany)
endif()

if(ElfloaderArmV8LeaveAarch64)
    # We need to build a aarch64 assembly file during an aarch32 build. We have
    # to write custom rules to do this as CMake doesn't support multiple compilers
    # within a single build config.
    find_program(AARCH64_COMPILER aarch64-linux-gnu-gcc)
    if("${AARCH64_COMPILER}" STREQUAL "AARCH64_COMPILER-NOTFOUND")
        message(
            FATAL_ERROR
                "Cannot find 'aarch64-linux-gnu-gcc' program. Use -DAARCH64_COMPILER=compiler"
        )
    endif()
    find_program(AARCH64_OBJCOPY aarch64-linux-gnu-objcopy)
    if("${AARCH64_OBJCOPY}" STREQUAL "AARCH64_OBJCOPY-NOTFOUND")
        message(
            FATAL_ERROR
                "Cannot find 'aarch64-linux-gnu-objcopy' program. Use -DAARCH64_OBJCOPY=objcopy"
        )
    endif()
    # Compile crt0_64.S and convert to a binary. This way the actual crt0.S can use
    # the .incbin directive and insert the aarch64 instructions before its own.
    add_custom_command(
        OUTPUT crt0_64.bin crt0_64.o
        COMMAND
            ${AARCH64_COMPILER} -I${CMAKE_CURRENT_SOURCE_DIR}/include/
            -I${CMAKE_CURRENT_SOURCE_DIR}/include/arch-arm/64/
            -I${CMAKE_CURRENT_SOURCE_DIR}/include/arch-arm/armv/armv8-a/64/ -c
            ${CMAKE_CURRENT_SOURCE_DIR}/src/arch-arm/32/crt0_64.S -o
            ${CMAKE_CURRENT_BINARY_DIR}/crt0_64.o
        COMMAND
            ${AARCH64_OBJCOPY} -O binary ${CMAKE_CURRENT_BINARY_DIR}/crt0_64.o crt0_64.bin
        DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/arch-arm/32/crt0_64.S
    )

    # We set the OBJECT_DEPENDS property on the crt0.S source file that tells CMake
    # any object files created from crt0.S also depend on crt0_64.bin. This causes
    # our builds to be rerun correctly.
    set(armv8_leave_arch_bin ${CMAKE_CURRENT_BINARY_DIR}/crt0_64.bin)
    set_property(SOURCE src/arch-arm/32/crt0.S PROPERTY OBJECT_DEPENDS ${armv8_leave_arch_bin})
endif()

if(KernelSel4ArchAarch64)
    # NEON registers aren't necessarily initialized for use before elfloader starts
    add_compile_options(-mgeneral-regs-only)
endif()

# Don't allow unaligned data store/load instructions as this will cause an alignment
# fault before the MMU is turned on.
if(KernelSel4ArchAarch64)
    add_compile_options(-mstrict-align)
elseif(KernelSel4ArchAarch32)
    add_compile_options(-mno-unaligned-access)
endif()

file(
    GLOB
        files
        src/*.c
        src/drivers/*.c
        src/drivers/smp/*.c
        src/drivers/uart/*.c
        src/utils/*.c
        src/arch-${KernelArch}/*.c
        src/arch-${KernelArch}/*.S
        src/arch-${KernelArch}/drivers/*.c
        src/plat/${KernelPlatform}/*.c
        src/binaries/elf/*.c
        src/arch-${KernelArch}/${KernelWordSize}/*.c
        src/plat/${KernelPlatform}/*.S
        src/arch-${KernelArch}/${KernelWordSize}/*.S
)

# We never want to give crt0_64.S to add_executable
list(FILTER files EXCLUDE REGEX src/arch-arm/32/crt0_64.S)

if(KernelArchARM)
    file(
        GLOB
            arm_files src/arch-${KernelArch}/armv/${KernelArmArmV}/${KernelWordSize}/*.c
            src/arch-${KernelArch}/armv/${KernelArmArmV}/${KernelWordSize}/*.S
    )
    list(APPEND files ${arm_files})
endif()

if(ElfloaderImageEFI)
    # We cannot control where EFI loads the image and so we must make it relocatable
    add_compile_options(-fpic)
    if(KernelSel4ArchAarch32)
        set(gnuefiArch "arm")
        # on aarch32 building with -fno-pie results in the compiler generating
        # movt/movw pairs that we can't easily relocate.
        add_compile_options(-fpie)
        # This flag is not supported by clang but add it in for gcc
        if(NOT CMAKE_C_COMPILER_ID STREQUAL "Clang")
            add_compile_options(-mno-single-pic-base)
        endif()
    else()
        set(gnuefiArch "aarch64")
        # on aarch64 building with -fno-pie will just use pc-relative addressing.
        add_compile_options(-fno-pie -fPIC)
    endif()

    file(GLOB efi_files src/binaries/efi/*.c)
    list(
        APPEND
            files
            ${efi_files}
            src/binaries/efi/gnuefi/crt0-efi-${gnuefiArch}.S
            src/binaries/efi/gnuefi/reloc_${gnuefiArch}.c
    )
    # We use gnu-efi's linker script on EFI.
    set(linkerScript ${CMAKE_CURRENT_LIST_DIR}/src/binaries/efi/gnuefi/elf_${gnuefiArch}_efi.lds)
else()
    add_compile_options(-fno-pic)
    add_compile_options(-fno-pie)
endif()

# Sort files to make build reproducible
list(SORT files)

set(cpio_files "")
list(APPEND cpio_files "$<TARGET_FILE:kernel.elf>")
if(ElfloaderIncludeDtb)
    list(APPEND cpio_files "${KernelDTBPath}")
endif()
list(APPEND cpio_files "$<TARGET_PROPERTY:rootserver_image,ROOTSERVER_IMAGE>")
if(NOT ${ElfloaderHashInstructions} STREQUAL "hash_none")
    set(hash_command "")
    if(ElfloaderHashSHA)
        set(hash_command "sha256sum")
    else()
        set(hash_command "md5sum")
    endif()
    add_custom_command(
        OUTPUT "kernel.bin"
        COMMAND
            bash -c
            "${hash_command} $<TARGET_FILE:kernel.elf> | cut -d ' ' -f 1 | xxd -r -p > ${CMAKE_CURRENT_BINARY_DIR}/kernel.bin"
        VERBATIM
        DEPENDS "$<TARGET_FILE:kernel.elf>"
    )
    add_custom_command(
        OUTPUT "app.bin"
        COMMAND
            bash -c
            "${hash_command} $<TARGET_PROPERTY:rootserver_image,ROOTSERVER_IMAGE> | cut -d ' ' -f 1 | xxd -r -p > ${CMAKE_CURRENT_BINARY_DIR}/app.bin"
        VERBATIM
        DEPENDS "$<TARGET_PROPERTY:rootserver_image,ROOTSERVER_IMAGE>"
    )
    list(APPEND cpio_files "${CMAKE_CURRENT_BINARY_DIR}/kernel.bin")
    list(APPEND cpio_files "${CMAKE_CURRENT_BINARY_DIR}/app.bin")
endif()

# Construct the ELF loader's payload.
MakeCPIO(archive.o "${cpio_files}" CPIO_SYMBOL _archive_start)

# If our platform has a YAML description, create a C header file to include
# information about the platform that is of interest to the ELF-loader, such as
# a physical memory map.
#
# We also need to put the ELF-loader's payload in memory at a place that will be
# out of the way of the kernel and user images that the elfloader extracts.  In
# other words, we don't want the ELF-loader (with its payload) to clobber
# itself.
#
# Formerly there was a complex set of conditionals driving a table of
# hard-coded addresses.
#
# Now, instead, we compute a reasonable image start address, using a tool called
# `shoehorn`, based on knowledge of how where and how big the extracted payloads
# will be, obtained by a tool called `elf_sift`.
set(PLATFORM_HEADER_DIR "${CMAKE_CURRENT_BINARY_DIR}/gen_headers")
set(CMAKE_TOOL_HELPERS_DIR "${CMAKE_CURRENT_LIST_DIR}/../cmake-tool/helpers")

if(DEFINED platform_yaml)
    set(PLATFORM_SIFT "${CMAKE_TOOL_HELPERS_DIR}/platform_sift.py")
    set(PLATFORM_INFO_H "${PLATFORM_HEADER_DIR}/platform_info.h")
    add_custom_command(
        OUTPUT ${PLATFORM_INFO_H}
        COMMAND ${PLATFORM_SIFT} --emit-c-syntax ${platform_yaml} > ${PLATFORM_INFO_H}
        VERBATIM
        DEPENDS ${platform_yaml} ${PLATFORM_SIFT}
    )
    set_property(SOURCE src/common.c PROPERTY OBJECT_DEPENDS ${PLATFORM_INFO_H})

    set(IMAGE_START_ADDR_H "${PLATFORM_HEADER_DIR}/image_start_addr.h")
    if(NOT "${IMAGE_START_ADDR}" STREQUAL "")
        file(WRITE ${IMAGE_START_ADDR_H} "#define IMAGE_START_ADDR ${IMAGE_START_ADDR}")
    else()
        # `shoehorn` calls `elf_sift`, so we'll need to depend on it.
        set(ELF_SIFT "${CMAKE_TOOL_HELPERS_DIR}/elf_sift.py")
        set(SHOEHORN "${CMAKE_TOOL_HELPERS_DIR}/shoehorn.py")
        set(ARCHIVE_O "${CMAKE_CURRENT_BINARY_DIR}/archive.o")
        add_custom_command(
            OUTPUT ${IMAGE_START_ADDR_H}
            COMMAND
                ${SHOEHORN} ${platform_yaml} ${ARCHIVE_O} > ${IMAGE_START_ADDR_H}
            VERBATIM
            DEPENDS
                ${ARCHIVE_O}
                ${platform_yaml}
                ${ELF_SIFT}
                ${SHOEHORN}
        )
    endif()
else()
    message(
        FATAL_ERROR
            "no image start address computed for platform; use platform YAML file ${platform_yaml} or replace this diagnostic with a CMake custom command to generate a file \"${IMAGE_START_ADDR_H}\" that hard-codes it; the same goes for \"${PLATFORM_INFO_H}\""
    )
endif()

if(DEFINED KernelDTBPath)
    get_filename_component(KernelTools ${HARDWARE_GEN_PATH} DIRECTORY)
    set(config_file "${KernelTools}/hardware.yml")
    set(schema_file "${KernelTools}/hardware_schema.yml")
    set(DEVICES_GEN_H "${PLATFORM_HEADER_DIR}/devices_gen.h")
    add_custom_command(
        OUTPUT ${DEVICES_GEN_H}
        COMMAND
            ${PYTHON3} ${HARDWARE_GEN_PATH}
            --elfloader
            --elfloader-out "${DEVICES_GEN_H}"
            --hardware-config "${config_file}"
            --hardware-schema "${schema_file}"
            --dtb "${KernelDTBPath}"
        VERBATIM
        DEPENDS ${KernelDTBPath} ${config_file} ${schema_file}
    )
    set_property(SOURCE src/drivers/driver.c PROPERTY OBJECT_DEPENDS ${DEVICES_GEN_H})
endif()

# Generate linker script
separate_arguments(c_arguments NATIVE_COMMAND "${CMAKE_C_FLAGS}")
# Add extra compilation flags required for clang
if(CMAKE_C_COMPILER_ID STREQUAL "Clang")
    list(APPEND c_arguments "${CMAKE_C_COMPILE_OPTIONS_TARGET}${CMAKE_C_COMPILER_TARGET}")
endif()
add_custom_command(
    OUTPUT "linker.lds_pp"
    COMMAND
        ${CMAKE_C_COMPILER} "${c_arguments}" "-I${PLATFORM_HEADER_DIR}"
        "-I$<JOIN:$<TARGET_PROPERTY:sel4_autoconf,INTERFACE_INCLUDE_DIRECTORIES>,;-I>"
        "-I$<JOIN:$<TARGET_PROPERTY:elfloader_Config,INTERFACE_INCLUDE_DIRECTORIES>,;-I>" -P -E -o
        linker.lds_pp -x c ${linkerScript}
    DEPENDS
        sel4_autoconf
        ${linkerScript}
        elfloader_Config
        ${IMAGE_START_ADDR_H}
    VERBATIM COMMAND_EXPAND_LISTS
)
add_custom_target(elfloader_linker DEPENDS linker.lds_pp)

add_executable(elfloader EXCLUDE_FROM_ALL ${files} archive.o)
if(ElfloaderImageEFI)
    set_property(TARGET elfloader APPEND_STRING PROPERTY LINK_FLAGS " -pie ")
    set_target_properties(elfloader PROPERTIES LINK_DEPENDS ${linkerScript})
    set_property(
        TARGET elfloader
        APPEND_STRING
        PROPERTY
            LINK_FLAGS
            # -Bsymbolic forces symbols to bind to their definitions within the elfloader
            # EFI_SUBSYSTEM=0xa indicates that we're building an EFI application.
            " -Wl,-T ${linkerScript} -nostdlib -shared -Wl,-Bsymbolic,--defsym=EFI_SUBSYSTEM=0xa -Wl,--build-id=none"
    )
else()
    set_target_properties(
        elfloader
        PROPERTIES LINK_DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/linker.lds_pp
    )
    add_dependencies(elfloader elfloader_linker)
    set_property(
        TARGET elfloader
        APPEND_STRING
        PROPERTY
            LINK_FLAGS
            " -Wl,-T ${CMAKE_CURRENT_BINARY_DIR}/linker.lds_pp -nostdlib -static -Wl,--build-id=none"
    )
endif()

target_include_directories(
    elfloader
    PRIVATE
        "include"
        "include/plat/${KernelPlatform}"
        "include/arch-${KernelArch}"
        "include/arch-${KernelArch}/${KernelWordSize}"
        "${CMAKE_CURRENT_BINARY_DIR}/gen_headers"
        "${CMAKE_CURRENT_BINARY_DIR}"
)
if(KernelArchARM)
    target_include_directories(
        elfloader
        PRIVATE
            "include/arch-${KernelArch}/armv/${KernelArmArmV}"
            "include/arch-${KernelArch}/armv/${KernelArmArmV}/${KernelWordSize}"
    )
endif()

target_link_libraries(
    elfloader
    PRIVATE
        cpio
        gcc
        elfloader_Config
        sel4_autoconf
)
